Lua脚本规范与常见报错

重要

本文中含有需要您注意的重要提示信息,忽略该信息可能对您的业务造成影响,请务必仔细阅读。

云数据库 Tair(兼容 Redis)实例支持Lua相关命令,通过Lua脚本可高效地处理CAS(compare-and-set)命令,进一步提升实例的性能,同时可以轻松实现以前较难实现或者不能高效实现的模式。本文介绍使用Lua脚本的基本语法与使用规范。

注意事项

数据管理服务DMS控制台目前暂不支持使用Lua脚本等相关命令,请通过客户端或redis-cli连接实例使用Lua脚本。

基本语法

命令

语法

说明

EVAL

EVAL script numkeys [key [key ...]] [arg [arg ...]]

执行给定的脚本和参数,并返回结果。

参数说明:

  • script:Lua脚本。

  • numkeys:指定KEYS[]参数的数量,非负整数。

  • KEYS[]:传入的Redis键参数。

  • ARGV[]:传入的脚本参数。KEYS[]与ARGV[]的索引均从1开始。

说明
  • 与SCRIPT LOAD命令一样,EVAL命令也会将Lua脚本缓存至实例。

  • 混用或滥用KEYS[]ARGV[]可能会导致实例产生不符合预期的行为,尤其在集群模式下,详情请参见集群架构中Lua脚本的限制

  • 推荐使用KEYS[]ARGV[]的方式传递参数。不推荐将参数编码进脚本中,过多类似行为会导致LUA虚拟机内存使用量上升,且无法及时回收,极端情况下会导致实例主库与备库内存溢出(Out of Memory),造成数据丢失。

EVALSHA

EVALSHA sha1 numkeys key [key ...] arg [arg ...]

给定脚本的SHA1校验和,实例将再次执行脚本。

使用EVALSHA命令时,若sha1值对应的脚本未缓存至Redis中,Redis会返回NOSCRIPT错误,请通过EVAL或SCRIPT LOAD命令将目标脚本缓存至Redis中后进行重试,详情请参见处理NOSCRIPT错误

SCRIPT LOAD

SCRIPT LOAD script

将给定的script脚本缓存在实例中,并返回该脚本的SHA1校验和。

SCRIPT EXISTS

SCRIPT EXISTS script [script ...]

给定一个(或多个)脚本的SHA1,返回每个SHA1对应的脚本是否已缓存在当前实例中。脚本已存在则返回1,不存在则返回0。

SCRIPT KILL

SCRIPT KILL

停止正在运行的Lua脚本。

SCRIPT FLUSH

SCRIPT FLUSH

清空当前实例中的所有Lua脚本缓存。

更多关于Redis命令的介绍,请参见Redis官网

以下为部分命令的示例,本文在执行以下命令前执行了SET foo value_test

  • EVAL命令示例:

    EVAL "return redis.call('GET', KEYS[1])" 1 foo

    返回示例:

    "value_test"
  • SCRIPT LOAD命令示例:

    SCRIPT LOAD "return redis.call('GET', KEYS[1])"

    返回示例:

    "620cd258c2c9c88c9d10db67812ccf663d96bdc6"
  • EVALSHA命令示例:

    EVALSHA 620cd258c2c9c88c9d10db67812ccf663d96bdc6 1 foo

    返回示例:

    "value_test"
  • SCRIPT EXISTS命令示例:

    SCRIPT EXISTS 620cd258c2c9c88c9d10db67812ccf663d96bdc6 ffffffffffffffffffffffffffffffffffffffff

    返回示例:

    1) (integer) 1
    2) (integer) 0
  • SCRIPT FLUSH命令示例:

    警告

    该命令会清空实例中的所有Lua脚本缓存,请提前备份Lua脚本。

    SCRIPT FLUSH

    返回示例:

    OK

优化内存、网络开销

现象:

在实例中缓存了大量功能重复的脚本,占用大量内存空间甚至引发内存溢出(Out of Memory),错误示例如下。

EVAL "return redis.call('set', 'k1', 'v1')" 0
EVAL "return redis.call('set', 'k2', 'v2')" 0

解决方案:

  • 请避免将参数作为常量写在Lua脚本中,以减少内存空间的浪费。

    # 与错误示例实现相同功能但仅需缓存一次脚本。
    EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 k1 v1
    EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 k2 v2
  • 更加建议采用如下写法,在减少内存的同时,降低网络开销。

    SCRIPT LOAD "return redis.call('set', KEYS[1], ARGV[1])"    # 执行后,Redis将返回"55b22c0d0cedf3866879ce7c854970626dcef0c3"
    EVALSHA 55b22c0d0cedf3866879ce7c854970626dcef0c3 1 k1 v1
    EVALSHA 55b22c0d0cedf3866879ce7c854970626dcef0c3 1 k2 v2

清理Lua脚本的内存占用

现象:

由于Lua脚本缓存将计入实例的内存使用量中,并会导致used_memory升高,当实例的内存使用量接近甚至超过maxmemory时,可能引发内存溢出(Out Of Memory),报错示例如下。

-OOM command not allowed when used memory > 'maxmemory'.

解决方案:

通过客户端执行SCRIPT FLUSH命令清除Lua脚本缓存,但与FLUSHALL不同,SCRIPT FLUSH命令为同步操作。若实例缓存的Lua脚本过多,SCRIPT FLUSH命令会阻塞实例较长时间,可能导致实例不可用,请谨慎处理,建议在业务低峰期执行该操作。

说明

在控制台上单击清除数据只能清除数据,无法清除Lua脚本缓存。

同时,请避免编写过大的Lua脚本,防止占用过多的内存;避免在Lua脚本中大批量写入数据,否则会导致内存使用急剧升高,甚至造成实例OOM。在业务允许的情况下,建议开启数据逐出(实例默认开启,模式为volatile-lru)节省内存空间。但无论是否开启数据逐出,实例均不会逐出Lua脚本缓存。

处理NOSCRIPT错误

现象:

使用EVALSHA命令时,若sha1值对应的脚本未缓存至实例中,实例会返回NOSCRIPT错误,报错示例如下。

(error) NOSCRIPT No matching script. Please use EVAL.

解决方案:

请通过EVAL命令或SCRIPT LOAD命令将目标脚本缓存至实例中后进行重试。但由于实例不保证Lua脚本的持久化、复制能力,在部分场景下仍会清除Lua脚本缓存(例如实例迁移、变配等),这要求您的客户端需具备处理该错误的能力,详情请参见脚本缓存、持久化与复制

以下为一种处理NOSCRIPT错误的Python Demo示例,该demo利用Lua脚本实现了字符串prepend操作。

说明

您可以考虑通过Python的redis-py解决该类错误,redis-py提供了封装Redis Lua的一些底层逻辑判断(例如NOSCRIPT错误的catch)的Script类。

import redis
import hashlib

# strin是一个Lua脚本的字符串,函数以字符串的格式返回strin的sha1值。
def calcSha1(strin):
    sha1_obj = hashlib.sha1()
    sha1_obj.update(strin.encode('utf-8'))
    sha1_val = sha1_obj.hexdigest()
    return sha1_val

class MyRedis(redis.Redis):

    def __init__(self, host="localhost", port=6379, password=None, decode_responses=False):
        redis.Redis.__init__(self, host=host, port=port, password=password, decode_responses=decode_responses)

    def prepend_inLua(self, key, value):
        script_content = """\
        local suffix = redis.call("get", KEYS[1])
        local prefix = ARGV[1]
        local new_value = prefix..suffix
        return redis.call("set", KEYS[1], new_value)
        """
        script_sha1 = calcSha1(script_content)
        if self.script_exists(script_sha1)[0] == True:      # 检查Redis是否已缓存该脚本。
            return self.evalsha(script_sha1, 1, key, value) # 如果已缓存,则用EVALSHA执行脚本
        else:
            return self.eval(script_content, 1, key, value) # 否则用EVAL执行脚本,注意EVAL有将脚本缓存到Redis的作用。这里也可以考虑采用SCRIPT LOAD与EVALSHA的方式。

r = MyRedis(host="r-******.redis.rds.aliyuncs.com", password="***:***", port=6379, decode_responses=True)

print(r.prepend_inLua("k", "v"))
print(r.get("k"))
            

处理Lua脚本超时

  • 现象:

    由于Lua脚本在实例中是原子执行的,Lua慢请求可能会导致实例阻塞。单个Lua脚本阻塞实例最多5秒,5秒后实例会给所有其他命令返回如下BUSY error报错,直到脚本执行结束。

    BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

    解决方案:

    您可以通过SCRIPT KILL命令终止Lua脚本或等待Lua脚本执行结束。

    说明
    • SCRIPT KILL命令在执行慢Lua脚本的前5秒不会生效(阻塞中)。

    • 建议您编写Lua脚本时预估脚本的执行时间,同时检查死循环等问题,避免过长时间阻塞实例导致服务不可用,必要时请拆分Lua脚本。

  • 现象:

    若当前Lua脚本已执行写命令,则SCRIPT KILL命令将无法生效,报错示例如下。

    (error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.

    解决方案:

    请在控制台的实例列表中单击对应实例重启

脚本缓存、持久化与复制

现象:

在不重启、不调用SCRIPT FLUSH命令的情况下,实例会一直缓存执行过的Lua脚本。但在部分情况下(例如实例迁移、变配、版本升级、切换等等),实例无法保证Lua脚本的持久化,也无法保证Lua脚本能够被同步至其他节点。

解决方案:

由于实例不保证Lua脚本的持久化、复制能力,请您在本地存储所有Lua脚本,在必要时通过EVAL或SCRIPT LOAD命令将Lua脚本重新缓存至实例中,避免实例重启、HA切换等操作时实例中的Lua脚本被清空而带来的NOSCRIPT错误。

集群架构中Lua脚本的限制

  • 为了保证Lua执行的原子性,Lua命令不可拆分,只能在集群架构的一个DB分片上执行。通常会根据Key来决定路由到哪个DB分片执行,所以在集群架构中执行Lua命令时至少需要指定一个Key。如果读写多个Key,则同一个Lua脚本中的Key必须属于同一个Slot,否则会导致执行结果异常。对于KEYS、SCAN、FLUSHDB等无Key的命令,虽然能正常执行,但返回结果只包含单个分片的数据。上述限制由Redis Cluster架构导致。

  • 对单个节点执行SCRIPT LOAD命令时,不保证将该Lua脚本存入至其他节点中。

代理模式(Proxy)自定义的Lua错误码及原因

Proxy会通过语法检查来提前识别Key跨越多个Slot的情况,提前暴露异常,方便问题排查。Proxy检查方法和Lua虚拟机存在差异,这导致了在Proxy中执行Lua命令会存在额外限制(例如不支持UNPACK命令、不支持在MULTI、EXEC事务中使用EVAL、EVALSHA、SCRIPT系列命令等)。您也可以通过关闭script_check_enable参数配置关闭Proxy对Lua语法的部分检查。

说明

关闭script_check_enable参数配置对实例有什么影响?

  • 当实例为兼容Redis 5.0版本(小版本5.0.8以下)、4.0及以下版本,不推荐关闭,可能会导致脚本执行结果错误但返回正确。

  • 其他版本关闭后,Proxy将不再检查Lua语法,但数据节点仍会正常检查Lua语法。

同时,读写分离架构实例如果开启了readonly_lua_route_ronode_enable配置,Proxy会检查Lua是否只包含只读命令并决定能否将Lua转发到只读节点,该检查逻辑对Lua语法存在限制。

具体错误码和原因请参见下表。

错误码分类

错误码

说明

Redis Cluster 架构限制

-ERR for redis cluster, eval/evalsha number of keys can't be negative or zero\r\n

执行Lua时必须带有Key,Proxy会根据Key决定将Lua转发到哪个DB分片上执行。

# 正确示例
EVAL "return redis.call('get', KEYS[1])" 1 fooeval

# 错误示例
EVAL "return redis.call('get', 'foo')" 0

-ERR 'xxx' command keys must in same slot

Lua脚本中的多个Key必须属于同一个Slot。

# 正确示例:
EVAL "return redis.call('mget', KEYS[1], KEYS[2])" 2 foo {foo}bar

# 错误示例:
EVAL "return redis.call('mget', KEYS[1], KEYS[2])" 2 foo foobar

Proxy Lua语法检查导致的额外限制

(关闭 script_check_enable 配置可以避免该检查)

-ERR bad lua script for redis cluster, nested redis.call/redis.pcall

不支持Redis嵌套方式调用,您可以使用局部变量的方式进行调用。

# 正确示例
EVAL "local value = redis.call('GET', KEYS[1]); redis.call('SET', KEYS[2], value)" 2 foo bar

# 错误示例
EVAL "redis.call('SET', KEYS[1], redis.call('GET', KEYS[2]))" 2 foo bar

-ERR bad lua script for redis cluster, first parameter of redis.call/redis.pcall must be a single literal string

redis.call/pcall中调用的命令必须是字符串常量。

# 正确示例
eval "redis.call('GET', KEYS[1])" 1 foo

# 错误示例
eval "local cmd = 'GET'; redis.call(cmd, KEYS[1])" 1 foo

-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array\r\n

所有Key都应该由KEYS数组来传递,redis.call/pcall中调用的命令,Key的位置必须是KEYS array,且不能使用Lua变量替换KEYS,。

说明

Redis开源版 5.0版本(小版本5.0.8以下)、4.0及以下版本实例或Proxy代理版本较低(云原生版7.0.2以下 、经典版6.8.12以下)存在该限制。

如果实例版本、代理版本都符合要求但仍存在限制,请修改任意参数(例如query_cache_expire参数),等待1分钟后重试。

# 正确示例:
EVAL "return redis.call('mget', KEYS[1], KEYS[2])" 2 foo {foo}bar

# 错误示例:
EVAL "return redis.call('mget', KEYS[1], '{foo}bar')" 1 foo                      # '{foo}bar'作为Key,应该使用KEYS数组进行传递。
EVAL "local i = 2 return redis.call('mget', KEYS[1], KEYS[i])" 2 foo {foo}bar    # 在代理模式(Proxy)不允许执行此脚本,因为KEYS数据的索引是变量,但在直连模式中无此限制。
EVAL "return redis.call('mget', KEYS[1], ARGV[1])" 1 foo {foo}bar                # 不应该使用ARGV[1]数据元素作为Key。

-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array, include destination, and KEYS should not be in expression

ZUNIONSTORE、ZINTERSTORE命令的destination参数必须用KEYS传递。

说明

Redis开源版 5.0版本(小版本5.0.8以下)、4.0及以下版本实例或Proxy代理版本较低(云原生版7.0.2以下 、经典版6.8.12以下)存在该限制。

如果实例版本、代理版本都符合要求但仍存在限制,请修改任意参数(例如query_cache_expire参数),等待1分钟后重试。

-ERR bad lua script for redis cluster, ZUNIONSTORE/ZINTERSTORE numkeys parameter should be a single number and not expression

ZUNIONSTORE、ZINTERSTORE命令的numkeys参数不是常量。

说明

Redis开源版 5.0版本(小版本5.0.8以下)、4.0及以下版本实例或Proxy代理版本较低(云原生版7.0.2以下 、经典版6.8.12以下)存在该限制。

如果实例版本、代理版本都符合要求但仍存在限制,请修改任意参数(例如query_cache_expire参数),等待1分钟后重试。

-ERR bad lua script for redis cluster, ZUNIONSTORE/ZINTERSTORE numkeys value is not an integer or out of range

ZUNIONSTORE、ZINTERSTORE命令的numkeys参数不是数字。

说明

Redis开源版 5.0版本(小版本5.0.8以下)、4.0及以下版本实例或Proxy代理版本较低(云原生版7.0.2以下 、经典版6.8.12以下)存在该限制。

如果实例版本、代理版本都符合要求但仍存在限制,请修改任意参数(例如query_cache_expire参数),等待1分钟后重试。

-ERR bad lua script for redis cluster, ZUNIONSTORE/ZINTERSTORE all the keys that the script uses should be passed using the KEYS array

ZUNIONSTORE、ZINTERSTORE命令的所有Key必须通过KEYS传递。

说明

Redis开源版 5.0版本(小版本5.0.8以下)、4.0及以下版本实例或Proxy代理版本较低(云原生版7.0.2以下 、经典版6.8.12以下)存在该限制。

如果实例版本、代理版本都符合要求但仍存在限制,请修改任意参数(例如query_cache_expire参数),等待1分钟后重试。

-ERR bad lua script for redis cluster, XREAD/XREADGROUP all the keys that the script uses should be passed using the KEYS array

XREAD、XREADGROUP命令的所有Key必须通过KEYS传递。

说明

Redis开源版 5.0版本(小版本5.0.8以下)、4.0及以下版本实例或Proxy代理版本较低(云原生版7.0.2以下 、经典版6.8.12以下)存在该限制。

如果实例版本、代理版本都符合要求但仍存在限制,请修改任意参数(例如query_cache_expire参数),等待1分钟后重试。

-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array, and KEYS should not be in expression, sort command store key does not meet the requirements

SORT命令的Key必须通过KEYS传递。

说明

Redis开源版 5.0版本(小版本5.0.8以下)、4.0及以下版本实例或Proxy代理版本较低(云原生版7.0.2以下 、经典版6.8.12以下)存在该限制。

如果实例版本、代理版本都符合要求但仍存在限制,请修改任意参数(例如query_cache_expire参数),等待1分钟后重试。

读写权限问题

-ERR Write commands are not allowed from read-only scripts

通过EVAL_RO命令发送的Lua中不能包含写命令。

-ERR bad write command in no write privilege

只读账号发送的Lua中不能包含写命令。

命令未支持

-ERR script debug not support

Proxy当前不支持SCRIPT DEBUG命令。

-ERR bad lua script for redis cluster, redis.call/pcall unkown redis command xxx

Lua中包含Proxy不支持的命令。更多信息请参见集群架构与读写分离架构实例的命令限制

Lua 语法错误

  • -ERR bad lua script for redis cluster, redis.call/pcall expect '('

  • -ERR bad lua script for redis cluster, redis.call/redis.pcall definition is not complete, expect ')'

Lua语法错误,redis.call后面必须包含完整的()

-ERR bad lua script for redis cluster, at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE

ZUNIONSTORE、ZINTERSTORE命令的numkeys参数必须大于0。

-ERR bad lua script for redis cluster, ZUNIONSTORE/ZINTERSTORE key count < numkeys

ZUNIONSTORE、ZINTERSTORE命令的实际Key数量小于numkeys值。

-ERR bad lua script for redis cluster, xread/xreadgroup command syntax error

XREAD、XREADGROUP命令的语法不对,请检查参数个数。

-ERR bad lua script for redis cluster, xread/xreadgroup command syntax error, streams must be specified

XREAD、XREADGROUP命令必须需要有streams参数。

-ERR bad lua script for redis cluster, sort command syntax error

SORT命令的语法错误。